package com.crunch.math;

/**
 * A class for planes in 3D, represented by the equation ax + by + cz = d. The vector (a,b,c) is considered the plane's
 * normal.
 */
public final class Planef {
	Vector3f normal;
	float d;

	/**
	 * Constructs a plane from the equation ax + by + cz = d.
	 */
	public Planef(Vector3f normal, float d) {
		this.normal = new Vector3f(normal);
		this.d = d;
	}

	/**
	 * Sets the plane from the equation ax + by + cz = d.
	 */
	public void set(Vector3f normal, float d) {
		this.normal.set(normal);
		this.d = d;
	}

	/**
	 * Constructs a plane by copying the one provided.
	 */
	public Planef(Planef p) {
		this.normal = new Vector3f(p.normal);
		this.d = p.d;
	}

	/**
	 * Sets the plane by copying the one provided.
	 */
	public void set(Planef p) {
		normal.set(p.normal);
		d = p.d;
	}

	/**
	 * Constructs a plane from three points. The points are assumed to be in counterclockwise winding order.
	 */
	public Planef(Vector3f a, Vector3f b, Vector3f c) {
		normal = MathOps.cross(b.subtract(a), c.subtract(a));
		d = MathOps.dot(a, normal);
	}

	/**
	 * Sets the plane from three points. The points are assumed to be in counterclockwise winding order.
	 */
	public void set(Vector3f a, Vector3f b, Vector3f c) {
		normal.set(MathOps.cross(b.subtract(a), c.subtract(a)));
		d = MathOps.dot(a, normal);
	}

	/**
	 * Constructs a plane from a point and a normal.
	 */
	public Planef(Vector3f point, Vector3f normal) {
		// ax + by + cz = d
		this.normal = new Vector3f(normal);
		d = MathOps.dot(point, normal);
	}

	/**
	 * Sets the plane from a point and a normal.
	 */
	public void setPointNormal(Vector3f point, Vector3f normal) {
		this.normal.set(normal);
		d = MathOps.dot(point, normal);
	}

	/**
	 * Returns the normal.
	 */
	public Vector3f getNormal() {
		return new Vector3f(normal);
	}

	/**
	 * Sets the normal.
	 */
	public void setNormal(Vector3f normal) {
		this.normal.set(normal);
	}

	/**
	 * Returns d.
	 */
	public float getD() {
		return d;
	}

	/**
	 * Sets d.
	 */
	public void setD(float d) {
		this.d = d;
	}

	/**
	 * Returns whether this plane is equal to the object {@code o}.
	 */
	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		} else if (o == null || !(o instanceof Planef)) {
			return false;
		} else {
			return equals((Planef) o);
		}
	}

	/**
	 * Returns a hash code for this object.
	 */
	@Override
	public int hashCode() {
		return normal.hashCode() ^ Float.floatToIntBits(d);
	}

	/**
	 * Returns whether this plane is equal to the plane {@code p}.
	 */
	public boolean equals(Planef p) {
		return normal.equals(p.normal) && d == p.d;
	}

	/**
	 * Returns a normalized version of this plane.
	 */
	public Planef normalize() {
		return new Planef(this).normalizeAssign();
	}

	/**
	 * Normalizes this plane.
	 */
	public Planef normalizeAssign() {
		float invMag = 1.0f / normal.magnitude();
		normal.multiply(invMag);
		d *= invMag;
		return this;
	}

	/**
	 * Computes the intersection between the plane and a ray. The intersection time is returned (in terms of the ray's
	 * origin and direction), or {@link Float#POSITIVE_INFINITY} if no intersection occurred.
	 */
	public float intersect(Ray3f ray) {
		// p = o + td
		// want: n.p = d
		// n.(o + td) = d
		// n.o + t*n.d = d
		// t = (d - n.o) / n.d
		float nDotD = MathOps.dot(normal, ray.getDirection());
		if (nDotD == 0.0f) {
			return Float.POSITIVE_INFINITY;
		} else {
			return (d - MathOps.dot(normal, ray.getOrigin())) / nDotD;
		}
	}

	/**
	 * Computes the distance between a point and the plane. The distance is scaled with the magnitude of the normal.
	 */
	float distance(Vector3f point) {
		return Math.abs(signedDistance(point));
	}

	/**
	 * Computes the signed distance between a point and the plane. The distance is scaled with the magnitude of the
	 * normal.
	 */
	float signedDistance(Vector3f point) {
		return MathOps.dot(normal, point) - d;
	}

	/**
	 * Returns which side of the plane a point is on: -1, 0, or 1 for behind, on, or in front of the plane, respectively.
	 */
	float side(Vector3f point) {
		return Math.signum(signedDistance(point));
	}
}